const cvs = document.querySelector('canvas');
const ctx = cvs.getContext("2d");

// color.js
// draw a list of box, with different colors.
// each box is around 100x50
// around as horizontal and then vertical
// COL and ROW

const RWIDTH = 100;
const RHEIGHT = 50;
// nearly constant
let COL = 6;
let ROW = 6;  // may change inside init() or initDebug()
assert(((COL * ROW) % 2) == 0, 'COL * ROW is non-even number');
const COLOR_LIST = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', ];
//                    0      1         2         3        4       5       6
const MAX_COLOR = COLOR_LIST.length;
const SX = RWIDTH;
const SY = RHEIGHT;
const ST = {
    SELECT: 0,
    LINE: 1,
    DROP: 2,
}
const FRAME_TIME = (1000 / 60);

const BUTTON_HEIGHT = RHEIGHT * 1.5;
const BUTTON_RESTART_X = SX;
const BUTTON_RESTART_Y = SY + (COL + 4) * RHEIGHT ;
const BUTTON_RESTART_X2 = BUTTON_RESTART_X + 300;
const BUTTON_RESTART_Y2 = BUTTON_RESTART_Y + BUTTON_HEIGHT;
const BUTTON_RESTART_WIDTH = BUTTON_RESTART_X2 - BUTTON_RESTART_X;


let board =[];
let raf = undefined;
let state = ST.SELECT;
let debugMsg = '';
let startTime = Date.now();
let usedTime = Date.now(); // will modify later

// ref: Web audio javascript
// https://stackoverflow.com/questions/1933969/sound-effects-in-javascript-html5
// some free sound (short preview is ok)
// https://elements.envato.com/video-game-bite-274BNQC
let soundFall = new Audio('water.mp3'); // buffers automatically when created
let soundWin = new Audio('win.mp3'); // 'win.mp3');
let soundLose = new Audio('lose.mp3');
let soundError = new Audio('error.mp3');


function assert(condition, msg) {
    if (condition==false) {
        throw Error(msg);
    }
}


// return true if two arrays are the same
function arrayCompare(array1, array2) {
    return array1.length === array2.length && array1.every(function(value, index) { return value === array2[index]})
}


// return the list for the big pair first
/**
 * 
 * @param {Number} c1 
 * @param {Number} r1 
 * @param {Number} c2 
 * @param {Number} r2 
 * @returns [cb, rb, cs, rs] = bigSmall(c1, r1, c2, r2);
 */
function bigSmall(c1, r1, c2, r2) {
    let cb, rb, cs, rs;
    let lll = [];
    if (r1 > r2) {
        rb = r1;
        cb = c1;
        rs = r2;
        cs = c2;
    } else {
        rb = r2;
        cb = c2;
        rs = r1;
        cs = c1;
    }
    lll[0] = cb;
    lll[1] = rb;
    lll[2] = cs;
    lll[3] = rs;

    return lll;
}


function drawColor(x, y, m){
    ctx.fillStyle = m;
    ctx.fillRect(x, y, RWIDTH, RHEIGHT);
}

// leftmost pixel x of grid x
function px(gx) {
    return SX + RWIDTH * gx;
}

// upmost pixel y of grid y
function py(gy) {
    return SY + RHEIGHT * ROW - RHEIGHT * gy;
}


// mid x of grid x
function mx(gx){
    let x = SX + RWIDTH * gx + RWIDTH/2;
    return x;
}

// mid y of grid y
function my(gy){
    let y = SY + RHEIGHT * ROW - RHEIGHT * gy + RHEIGHT/2;
    return y;
}

function gridX(x) {
    let gx;
    gx = Math.floor((x - SX) / RWIDTH); // cannot use | 0;
    return gx;
}


function gridY(y) {
    let gy;
    gy = Math.floor(ROW - ((y - SY) / RHEIGHT)) + 1;
    return gy;
}


function gridValid(gx, gy) {
    if (gx < 0 || gx >= board.length) {
        return false;
    }
    if (gy < 0 || gy >= board[gx].length) {
        return false;
    }
    return true;
}


/**
 * now it will auto swap c1, c2 if c1 > c2
 * @param {Number} c1   smaller one
 * @param {Number} c2 
 * @returns max height between c1 and c2, non-inclusive
 */
function maxHeight(c1, c2) {
    let max = 0;
    if (c1 > c2) {
        let t = c1;
        c1 = c2;
        c2 = t;
    }
    for (let i=c1 + 1; i < c2; i++) {
        if (colHeight(i) > max) {
            max = board[i].length;
        }
    }
    return max; // can be zero
}

function isTop(c, r) {
    return (r == board[c].length - 1);
}


/**
 * 
 * find the first blocker column index 
 * between c1 => c2  non-inclusive (c1, c2)
 * on the level row r. 
 * NOTE c1 and c2 may NOT be increasing order
 * e.g. c1 > c2 OR c1 < c2 is possible.
 * beware of c1==c2 case, early exit -1
 * @param {Number} c1 
 * @param {Number} c2 
 * @param {Number} r 
 * @returns col index of blocker, OR -1 when no block 
 */
function findBlocker(c1, c2, r) {
    let s = Math.sign(c2 - c1);
    // s==1 when c1 < c2 (increasing order)
    // s==-1 when c2 > c1 (decreasing order)
    if (s==0) {
        return -1;
    }
    // adjacent or same column
    if (Math.abs(c1 - c2) < 2) {
        return -1;
    }

    // use endless to guard against endless loop
    let c = c1 + s;
    for (let endless = 0;  endless < COL; endless++) {
        // reach the end
        if (c == c2 - s) {
            return -1;
        }
        if (c < 0 || c >= board.length) {
            return -1;
        }

        // core logic
        if (board[c].length - 1 >= r) {
            return c;
        }
        c += s;  // next column
    }

    return -1;
}


function drawButtonRestart() {
    // 2-level button : restart
    // assume 32px courier already set
    ctx.font = '24px courier';
    ctx.fillStyle = 'black';
    ctx.fillRect(BUTTON_RESTART_X, BUTTON_RESTART_Y, BUTTON_RESTART_WIDTH, BUTTON_HEIGHT);
    ctx.fillStyle = 'silver';
    ctx.fillRect(BUTTON_RESTART_X + 2, BUTTON_RESTART_Y + 2, BUTTON_RESTART_WIDTH - 4, BUTTON_HEIGHT - 4);
    ctx.fillStyle = 'black';
    ctx.fillText('重来 Restart', BUTTON_RESTART_X + 10, BUTTON_RESTART_Y2 - BUTTON_HEIGHT / 2);
}


function drawBoard(){
    for(let c = 0; c < COL; c++){
        // r not end with ROW
        for(let r = 0; r < board[c].length; r++){
            let x, y;
            let color = COLOR_LIST[board[c][r]];
            ctx.fillStyle = color;
            x = SX + RWIDTH * c;
            // here, we do NOT use -(r+1)*RHEIGHT
            // so all the logic do NOT need (r+1)
            y = SY + ROW * RHEIGHT - r * RHEIGHT;
            // y = SY + RHEIGHT * r;
            ctx.fillRect(x, y, RWIDTH, RHEIGHT);
            ctx.strokeStyle = 'black';
            ctx.lineWidth = 1;
            ctx.strokeRect(x, y, RWIDTH, RHEIGHT);

        }
    }

    // draw the number of axis
    ctx.fillStyle = 'black';
    ctx.font = '32px courier';
    for (let c = 0; c < COL; c++) {
        let x, y;
        x = SX + RWIDTH * c + RWIDTH / 2 - 10;
        y = SY - 10;
        ctx.fillText(c, x, y);
    }
    for (let r = 0; r < ROW; r++) {
        let x, y;
        x = SX - RWIDTH + RWIDTH / 2 - 30;
        y = SY + ROW * RHEIGHT - RHEIGHT * r + RHEIGHT / 2 + 10;
        ctx.fillText(r, x, y);
    }
}


/**
 * 
 * @param {Number} c 
 * @returns height of column c, -1 if invalid column c
 */
function colHeight(c) {
    if (c < 0 || c >= COL) {
        return -1;
    }
    return board[c].length;
}

/**
 * (c1, r1) position of box1
 * (c2, r2) position of box2
 * draw the connecting line between box1 and box2
 * may have 0, 1 or 2 angles (90 degrees)
 * NO box in the middle of line
 * e.g.
 * CCCCCCC case
 * |-----|   line of 2 angles
 * r b b r
 * c o c o
 * g b g b
 * 
 * AAAAAA case
 * after some steps
 * c-----c   line of 0 angle
 * g b g b
 * 
 * another e.g.
 * 
 * BBBBBBB case
 * b-|      line of 1 angle 
 * r b
 * @param {Number} c1 
 * @param {Number} r1 
 * @param {Number} c2 
 * @param {Number} r2 
 */
function drawLine(c1, r1, c2, r2){
    let t;
    if(r1 > r2){
        t = r1;
        r1 = r2;
        r2 = t;
        t = c1;
        c1 = c2;
        c2 = t;
    }
    // after this line: assert(r1<=r2)
    // but no guarantee order of c1, c2
    // AAAAAAAA case
    if(r1 == r2){
        if (c1 > c2) {
            t = c1;
            c1 = c2;
            c2 = t;
        }
        // -1 means draw a straight line
        let maxH = maxHeight(c1, c2);
        assert(c1 <= c2, 'c1 <= c2');
        // if maxH < r1 : draw a straight line
        // else (maxH >= r1) : draw a |---|, maxH++

        // straight line
        // XXX wrong (max<r1 ?): correct: max <= r1
        if (maxH <= r1) { // maxH == 0 means nothing in between
            // console.log("AAAAAAAA straight line maxH, r1 : " + maxH + ', ' + r1);
            ctx.moveTo(mx(c1), my(r1));
            ctx.lineTo(mx(c2), my(r1));
            ctx.stroke();
            return;
        }
        assert(maxH >= r1, "maxH >= r1");
        // assert max >= r1
        // max++;
        // check if c1 and c2 are both on TOP
        // in this case, we draw line above them
        // if (board[c1].length==(r1+1) && board[c2].length==(r2+1)) {
        if (isTop(c1, r1) && isTop(c2, r2)) {
            // console.log('AAAA_BBBB 2 angles');
            ctx.moveTo(mx(c1), my(r1));
            ctx.lineTo(mx(c1), my(maxH));
            ctx.lineTo(mx(c2), my(maxH));
            ctx.lineTo(mx(c2), my(r2));
            ctx.stroke();
            return;
        }

        assert(r1 == 0, 'AAAA_CCCC r1 == 0');
        // console.log('AAAA_CCCC 2 angles down');
        // shall we check r1-1 is empty or NOT?
        ctx.moveTo(mx(c1), my(r1));
        ctx.lineTo(mx(c1), my(r1-1));
        ctx.lineTo(mx(c2), my(r1-1));
        ctx.lineTo(mx(c2), my(r1));
        ctx.stroke();
        return; // early exit
    }

    // BBBB
    if(c1 == c2){
        if(r1 > r2){
            let t = r1;
            r1 = r2;
            r2 = t;
        }
        assert(r1 <= r2, 'r1 <= r2');
        // BBBB_AAAA
        if(Math.abs(r2 - r1) <= 1){
            // console.log('BBBB_AAAA straight line');
            ctx.moveTo(mx(c1), my(r1));
            ctx.lineTo(mx(c1), my(r2));
            ctx.stroke(); 
            return;
        }
        // BBBB_BBBB left vertical line
        // simple C-shape left
        if (c1 <= 0 || r1 >= board[c1-1].length) {
            // console.log('BBBB_BBBB left vertical line');
            ctx.moveTo(mx(c1), my(r1));
            ctx.lineTo(mx(c1-1), my(r1));
            ctx.lineTo(mx(c1-1), my(r2));
            ctx.lineTo(mx(c1), my(r2));
            ctx.stroke();
            return;
        }
        // BBBB_CCCC right vertical line, simple C-shape right
        if(c1 >= board.length - 1 || r1 >= board[c1+1].length){
            // console.log('BBBB_CCCC right vertical line');
            ctx.moveTo(mx(c1), my(r1));
            ctx.lineTo(mx(c1+1), my(r1));
            ctx.lineTo(mx(c1+1), my(r2));
            ctx.lineTo(mx(c1), my(r2));
            ctx.stroke();
            return;
        }
        assert(false, 'BUG BBBB_XXXX impossible case: ', c1, r1, c2, r2);
        return; // early exit
    }


    if (c1 > c2) {
        // exchange in pair
        t = c1;
        c1 = c2;
        c2 = t;
        t = r1;
        r1 = r2;
        r2 = t;
    }
    // assert(c1 < c2); // include logic above AAAAAAAA
    let maxH = maxHeight(c1, c2);
    if (maxH - 1 >= r1 && maxH - 1 >= r2) {
        // console.log('CCCC_AAAA' + r1 + ' => ' + r2);
        // NOTE actually no relationship with r1 > r2
        if (isTop(c1, r1)==false) {
            console.log('BUG c1 r1 isTop==false');
            return;
        }
        if (isTop(c2, r2)==false) {
            console.log('BUG c2 r2 isTop==false');
            return;
        }
        // both are top
        ctx.moveTo(mx(c1), my(r1));
        ctx.lineTo(mx(c1), my(maxH));
        ctx.lineTo(mx(c2), my(maxH));
        ctx.lineTo(mx(c2), my(r2));
        ctx.stroke();
        return;
    }

    // rb >= rs
    let cb, rb, cs, rs;
    [cb, rb, cs, rs] = bigSmall(c1, r1, c2, r2);
    assert(rb >= rs, 'rb >= rs');
    let s = Math.sign(cs - cb);

    // assert(maxH - 1 < r1 || maxH - 1 < r2)
    // -|    |-   L-shape
    if (maxH - 1 < rb && isTop(cs, rs)) {
        // -| or |- are the same
        // console.log('CCCC_BBBB L-shape -| OR |-');
        ctx.moveTo(mx(cb), my(rb));
        ctx.lineTo(mx(cs), my(rb));
        ctx.lineTo(mx(cs), my(rs));
        ctx.stroke();
        return;
    }

    // complex C-shape or Snake-shape
    // --|   |--
    //   |   |
    // --|   |--
    // also good for snake shape
    if (true) { // (maxH - 1 >= rs) {
        console.log('CCCC_DDDD complex C-shape ', cb, rb, cs, rs);
        let sss = s;
        if ((colHeight(cs + s) - 1) >= rs) {
            sss = -s;
        }
        // if (colHeight(cb + sss) - 1 > rs) {
        //     sss = cs - cb - 1;
        // }
        if (colHeight(cs) - 1 < rb || colHeight(cb + sss) - 1 > rs) {
            console.log('small start');
            ctx.moveTo(mx(cs), my(rs));
            ctx.lineTo(mx(cs + sss), my(rs));
            ctx.lineTo(mx(cs + sss), my(rb));
            ctx.lineTo(mx(cb), my(rb));
        } else {
            console.log('big start');
            ctx.moveTo(mx(cb), my(rb));
            ctx.lineTo(mx(cb + sss), my(rb));
            ctx.lineTo(mx(cb + sss), my(rs));
            ctx.lineTo(mx(cs), my(rs));
        }
        ctx.stroke();
        return;
    }
}


function checkLose() {
    // lose only in select state
    if (0 == board.length) {
        return false;
    }
    let total = 0;
    let check = false;
    for (let c1 = 0; c1 < COL; c1++) {
        total += board[c1].length;
        for (let r1 = 0; r1 < ROW; r1 ++) {
            for (let c2 = 0; c2 < COL; c2++) {
                for (let r2 = 0; r2 < ROW; r2 ++) {
                    if (c1 == c2 && r1 == r2) {
                        continue; // skip same position
                    }
                    check = checkPair(c1, r1, c2, r2);
                    if (check == true) {
                        console.log('checkLose pair:', c1, r1, c2, r2);
                        return false; // still have pair
                    }
                }
            }

        }
    }
    if (0 == total) {
        return false; // win case, no lose
    }
    return true;
}


function checkWin() {
    // empty board no win
    if (board.length <= 0) {
        return false;
    }

    for (let c = 0; c < COL; c++) {
        if (board[c].length > 0) {
            return false;
        }
    }
    return true;
}


let lastTime = Date.now() + 1000;
let lastFps = 0, fps = 0;
function drawState() {
    let msg = '';
    let now = Date.now();

    if (now > lastTime) {
        lastFps = fps;
        fps = 0;
        lastTime = now + 1000;
    } else {
        fps ++;
    }
    let elapse = Math.floor((now - startTime) / 1000);
    msg += 'State:' + state + ' 秒:' + elapse + '  ' + debugMsg;
    // TODO checkLose() (no more move)
    ctx.font = '24px courier';
    ctx.fillStyle = 'silver';
    ctx.fillRect(SX, SY + RHEIGHT * (ROW + 2), 800, RHEIGHT * 1.5);

    ctx.fillStyle = 'black';
    if (debugMsg == 'win') {
        msg = 'WIN WIN WIN 胜利 !!! 用时(秒): ' + Math.floor(usedTime / 1000);
        ctx.font = '32px courier';
        ctx.fillStyle = 'red';
    }
    if (debugMsg == 'lose') {
        msg = 'LOSE 输了 不能消除';
        ctx.font = '32px courier';
        ctx.fillStyle = 'red';
    }
    ctx.fillText(msg, SX + 10, SY + RHEIGHT * (ROW + 3));
}

function initTestCheckPair() {
    board = [];
    board[0] = [2];
    board[1] = [0, 1, 2, 3];
    board[2] = [3, 0];
    board[3] = []
    board[4] = [1, 1, 1, 1, 1];
    board[5] = [3, 1];
    COL = board.length;
    ROW = maxHeight(-1, COL); // one more, because non-inclusive
}

function testCheckPair() {
    initTestCheckPair();
    drawBoard();

    let check;
    let gx1, gy1, gx2, gy2;
    [gx1, gy1] = [1, 0];
    [gx2, gy2] = [2, 1];

    check = checkPair(gx1, gy1, gx2, gy2);
    console.log('111 check: ', check, gx1, gy1, gx2, gy2);
    assert(check == false, '111 check');
    
    [gx1, gy1] = [1, 1];
    [gx2, gy2] = [2, 0];
    check = checkPair(gx1, gy1, gx2, gy2);
    console.log('222 check: ', check, gx1, gy1, gx2, gy2);
    assert(check == false, '222 check');

    // green top/left - green bottom/right
    [gx1, gy1] = [1, 3];
    [gx2, gy2] = [2, 0];
    check = checkPair(gx1, gy1, gx2, gy2);
    console.log('333 check: ', check, gx1, gy1, gx2, gy2);
    assert(check == true, '333 green check');

    [gx1, gy1] = [1, 1];
    [gx2, gy2] = [4, 4];
    check = checkPair(gx1, gy1, gx2, gy2);
    console.log('444 check: ', check, gx1, gy1, gx2, gy2);
    assert(check == true, '444 orange check');
    

}


// now the board is using board[col][row] order
// row=0 is the lowest row, bottom one.
// this is 4 col x 3 row(may have less) debug example
function initTestDrawLine() {
// const COLOR_LIST = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple', ];
//                       0      1         2         3        4       5       6
    board = [];
    // user-view
    // 4 5 1 1
    // 3 5 4 3  
    // 0 2 0 2
    //
    // c b o o b
    // g b c g b
    // r y r y r

    board[0] = [0, 4, 3];
    board[1] = [2, 5];
    board[2] = [0, 4, 5, 4, 5, 0];
    board[3] = [2, 5, 4];
    board[4] = [3,  ];
    board[5] = [3, 4, 5,  ];
    board[6] = [5,  ];
    board[7] = [0, 5, 0, 0, 5];
    COL = board.length;
    ROW = maxHeight(-1, COL); // one more, because non-inclusive

    // delete item(s)
    // board[0].splice(1, 1); // delete from index 1, delete count=1
    // board[0].splice(0, 2);  // delete from index 0, delete count=2
    // console.log(board[0].length); // check the len

    // add (append) an item
    // board[0][board[0].length] = 0; // give a red
    // console.log("after add red: " + board[0].length); // check the len

}


function testDrawLine() {
    initTestDrawLine();
    drawBoard();
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 1;
    // testcase KEEP
    // group 1 can draw, no overlap, some connected
    // group 2 complete no overlap, no connected
    // select group1 or group2 by comment out groupX = true
    let group1 = false, group2 = false;
    // group1 = true;
    // group2 = true;
    if (group1) {
        drawLine(5, 0, 4, 0); // green adjacent - 
        drawLine(0, 0, 2, 0);  // red and red |_|
        drawLine(0, 2, 4, 0);  // green and green |-|
        drawLine(2, 4, 7, 4);  // blue-blue straight
        drawLine(5, 2, 6, 0);  // -|
        drawLine(2, 2, 1, 1);  // |-
    }
    group2 = true;
    if (group2) {
        drawLine(7, 0, 7, 2); // simple C-shape (right)
        drawLine(2, 2, 2, 4); // simple C-shape (left)
        drawLine(0, 1, 2, 3); // complex C-shape (left)
        drawLine(2, 5, 7, 3); // complex C-shape (right)
        drawLine(3, 1, 7, 4); // Snake-shape (left)
        drawLine(5, 2, 7, 1); // Snake-shape (right)
        
        // this big snake overlap with 2 lines above
        // drawLine(2, 4, 7, 1); // Snake-shape big (right)
    }

    // bug case: keep
    // drawLine(2, 3, 5, 1);
}


function testEqual() {
    let a = 1.5;  // number
    let b = '1.5'; // string

    assert(a==b, 'two equal');
    // three equal means same value AND same type
    assert((a===b) == false, 'three equal');
    return true;
}


function testBigSmall() {
    let c1, r1, c2, r2;
    let cb, rb, cs, rs;
    let lst;

    [c1, r1] = [1, 1];
    [c2, r2] = [2, 2];
    // console.log('c1, r1, c2, r2 = ', c1, r1, c2, r2);
    lst = bigSmall(c1, r1, c2, r2);
    // console.log('lst=' + lst);
    // cb = list[0]; rb = list[1];
    [cb, rb, cs, rs] = lst;
    // cb = lst[0];

    assert(cb == 2 && rb == 2 && cs == 1 && rs == 1 , "111 bigSmall");

    [c1, r1] = [2, 3];
    [c2, r2] = [4, 2];
    lst = bigSmall(c1, r1, c2, r2);
    assert(arrayCompare([2, 3, 4, 2], lst), '222 bigSmall');

    console.log('GOOD testBigSmall');
    return true;
}


// XXX obsolete
function testFindBlocker() {
    let blocker;
    blocker = findBlocker(2, 6, 1); // expect 4
    console.log('111 blocker=' + blocker);
    assert(blocker==4, '111 blocker');
    blocker = findBlocker(6, 0, 2); // expect 2
    console.log('222 blocker=' + blocker);
    blocker = findBlocker(6, 2, 3); // expect -1
    console.log('333 blocker=' + blocker);
    blocker = findBlocker(2, 3, 1); // expect -1
    console.log('444 blocker=' + blocker);
    blocker = findBlocker(1, -10, 2); // expect -1
    console.log('555 blocker=' + blocker);
    board[-1.5] = 'hello';
    console.log('board[-1.5] : ' + board[-1.5]);

    assert(false, 'tsetFindBlocker not ready');
}


/**
 * 
 * @param {Number} max 
 * @returns two numbers in array, value 0 to (max-1), two numbers are different
 */
function randomTwoDiff(max) {
    if (max < 1) {
        throw new Error("max must be at least 2 to generate two distinct numbers");
    }

    // Generate the first number
    const num1 = myRandom(max); // Math.floor(Math.random() * max);

    // Generate the second number, ensuring it's not equal to the first
    const num2 = (num1 + 1 + myRandom(max-1)) % max;
    // tested: when max==2, it may return [0, 1] or [1, 0]
    // if (max==2) {
    //     console.log('!!!! num1, num2=', num1, num2);
    // }

    return [num1, num2];
}


/**
 * 
 * @param {Number} max positive integer
 * @returns a random number between 0 to <max
 * 
 */
function myRandom(max) {
    // simplest and normal case
    // return (Math.random() * max) | 0;

    // create a "fixed" random generator
    if ( typeof myRandom.seed == 'undefined' ) {
        // init seed here, can modify
        myRandom.seed = 6487;
    } else {
        myRandom.seed = (((myRandom.seed + 1) * 17) & 0xffff);
    }
    return Math.abs(myRandom.seed) % max;
}


// get color index
// @returns number of color, OR undefined if out-of-bound
function getColor(gx, gy) {
    if (gx < 0 || gx > board.length - 1) {
        return undefined;
    }
    if (gy < 0 || gy > board[gx].length -1) {
        return undefined;
    }
    return board[gx][gy];
}

/**
 * 
 * check the pair of position (gx1, gy2)
 * and (gx2, gy2), can or cannot be removed
 * @param {Number} gx1 
 * @param {Number} gy1 
 * @param {Number} gx2 
 * @param {Number} gy2 
 * @returns true if the pair can be removed
 */
function checkPair(gx1, gy1, gx2, gy2) {
    let maxH;
    let color1, color2;
    maxH = maxHeight(gx1, gx2);
    color1 = getColor(gx1, gy1);
    color2 = getColor(gx2, gy2);
    if (color1 == undefined || color2 == undefined) {
        return false;
    }
    if (color1 != color2) {
        return false;
    }

    // aaaa same row
    if (gy1 == gy2) {
        if (gy1 > maxH - 1) {
            console.log('aaa-aaa straight line');
            return true;
        }
        // base is ok, even with blocking
        if (gy1 == 0) {
            console.log('aaa-bbb base');
            return true;
        }
        // adjacent, next to each other
        if (Math.abs(gx1 - gx2) <= 1) {
            console.log('aaa-ccc adjacent');
            return true;
        }
        // here, there are blocking on horizontal line
        // when both are isTop, return true
        if (isTop(gx1, gy1) && isTop(gx2, gy2)) {
            console.log('aaa-ddd both isTop');
            return true;
        }
        console.log('aaa-fff false case');
        return false;
    }

    let xSmall, xBig, ySmall, yBig;
    // according to big-small order of y (r)
    [xBig, yBig, xSmall, ySmall] = bigSmall(gx1, gy1, gx2, gy2);

    // bbb same column
    if (gx1 == gx2) {
        if (Math.abs(gy1 - gy2) <= 1) {
            // console.log('bbb-aaa adjacent');
            return true;
        }
        if (ySmall > colHeight(gx1 - 1) - 1 || ySmall > colHeight(gx1 + 1) -1) {
            console.log('bbb-bbb noblock on either side');
            return true;
        }
        console.log('bbb-fff false case');
        return false;
    }

    // ccc - row and col are not the same

    // Hat-shape
    if (isTop(gx1, gy1) && isTop(gx2, gy2)) {
        console.log('ccc-aaa hat |-|');
        return true;
    }

    // L-shape
    if (maxH - 1 < yBig && isTop(xSmall, ySmall)) {
        console.log('ccc-lll L-shape |-');
        return true;
    }

    // complex C-shape
    let s = Math.sign(xSmall - xBig);
    // console.log('C-shape sign s=', s);
    // <= 0 include -1 case
    if (colHeight(xSmall + s) - 1 < ySmall) {
        let m;
        // maxHeight is non-inclusive, so we need 
        // to have two  + s + s
        m = maxHeight(xBig, xSmall + s + s);
        // console.log('C-shape m, yBig: ', m, yBig);
        if (m - 1 < yBig) {
            console.log('ccc-bbb complex C-shape');
            return true;
        }
    }

    // Snake-shape
    if (ySmall > colHeight(xSmall - s) - 1) {
        console.log('ccc-ccc Snake-shape');
        return true;
    }

    console.log('fff-false: ', gx1, gy1, gx2, gy2);
    return false;  // default cannot
}

// offset is on pixel y
function drawBoxOffset(c, r, color, offset) {
    let x, y;
    ctx.fillStyle = color;
    x = SX + RWIDTH * c;
    // here, we do NOT use -(r+1)*RHEIGHT
    // so all the logic do NOT need (r+1)
    y = SY + ROW * RHEIGHT - r * RHEIGHT + offset; // core !
    ctx.fillRect(x, y, RWIDTH, RHEIGHT);
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 1;
    ctx.strokeRect(x, y, RWIDTH, RHEIGHT);
}

class Drop {
    /**
     * 
     * @param {Array} list 
     * @returns 
     */
    constructor(list) {
        if (list==undefined) {
            [this.xBig, this.yBig, this.xSmall, this.ySmall] = [-1, -1, -1, -1];
            this.bigList = [];
            this.smallList = [];
            return;
        }
        [this.xBig, this.yBig, this.xSmall, this.ySmall] = bigSmall(list[0], list[1], list[2], list[3]);
        // board[this.xBig].splice(this.yBig, 1); // testing
        // board[this.xSmall].splice(this.ySmall, 1); // testing
        // return;

        this.bigList = [];
        for (let i = this.yBig + 1; i < board[this.xBig].length; i++) {
            this.bigList.push(board[this.xBig][i]);
        }
        board[this.xBig].splice(this.yBig, board[this.xBig].length - this.yBig);
        // TODO board[x].length - this.xBig ???

        this.smallList = [];
        let upperEnd;
        if (this.xBig == this.xSmall) {
            upperEnd = this.yBig;
        } else {
            upperEnd = board[this.xSmall].length;
        }

        for (let i = this.ySmall + 1; i < upperEnd; i++) {
            this.smallList.push(board[this.xSmall][i]);
        }
        // same column, only delete from yBig to ySmall
        board[this.xSmall].splice(this.ySmall, upperEnd - this.ySmall);

        console.log('bigList: ', this.bigList);
        console.log('smallList: ', this.smallList);
        this.counter = 0;

        soundFall.play(); // blocking???
    }

    draw() {
        // must have clear
        ctx.clearRect(0, 0, cvs.clientWidth, cvs.clientHeight);
        drawBoard();
        // counter add in logic

        // small first
        let c = this.xSmall;
        for(let r = 0; r < this.smallList.length; r++){
            let color = COLOR_LIST[this.smallList[r]];
            drawBoxOffset(c, r + this.ySmall + 1, color, this.counter);
        }

        // big list second
        c = this.xBig;
        for(let r = 0; r < this.bigList.length; r++){
            let color = COLOR_LIST[this.bigList[r]];
            // same column
            if (this.xBig == this.xSmall) {
                drawBoxOffset(c, r + this.yBig + 1, color, this.counter * 2);
            } else {
                drawBoxOffset(c, r + this.yBig + 1, color, this.counter);
            }
        }
    }

    logic() {
        this.counter ++;

        // when both list are empty, skip animation
        if (this.counter < RHEIGHT && (this.smallList.length + this.bigList.length) > 0) {
            // continue
            return -1;
        }

        // order is important small first, then big
        for (let i=0; i<this.smallList.length; i++) {
            board[this.xSmall].push(this.smallList[i]);
        }
        for (let i=0; i<this.bigList.length; i++) {
            board[this.xBig].push(this.bigList[i]);
        }
        drawBoard(); // safety
        state = ST.SELECT; // for debug
        selectState.init([]); 
        return ST.SELECT; 
    }
}
let dropState = new Drop(undefined);


class Line {
    init(list) {
        if (list==undefined) {
            [this.gx1, this.gy1, this.gx2, this.gy2] = [-1, -1, -1, -1];
            return;
        }
        [this.gx1, this.gy1, this.gx2, this.gy2] = list;
        this.counter = 0;
        console.log('Line.init: ', this.gx1, this.gy1, this.gx2, this.gy2);
    }


    constructor(list) {
        this.init(list);
        return;
    }


    logic() {
        // roughly 2 seconds
        this.counter = (this.counter + 1) & 0xffff;;
        if (this.counter > 32) {
            dropState = new Drop([this.gx1, this.gy1, this.gx2, this.gy2]);
            return ST.DROP;
        }
        return -1;
    }

    draw() {
        if (this.gx1 == -1) {
            return; // no draw
        }
        ctx.clearRect(0, 0, cvs.clientWidth, cvs.clientHeight);
        drawBoard();

        const FLASH_FRAME = 8; // must be power of 2
        // flash 2 colors
        if ((this.counter & FLASH_FRAME) == FLASH_FRAME) {
            ctx.strokeStyle = 'black';
        } else {
            ctx.strokeStyle = 'white';
        }
        ctx.lineWidth = 1;
        ctx.beginPath();
        drawLine(this.gx1, this.gy1, this.gx2, this.gy2);
        ctx.closePath();
    }
}
let lineState = new Line(undefined);


class Select {

    init(param) {
        // usually param == undefined (no selection)
        if (undefined == param) {
            this.pos = [];  // empty array
        } else {
            this.pos = param; // assume it is an array
        }
        // console.log('new Select.init()');
        // none is selected: pos = [] OR undefined
        // one is selected: pos = [c1, r1]
        // two is selected: pos = [c1, r1, c2, r2]

        this.counter = 0;
        let check;
        check = checkLose();
        console.log('checkLose : ', check);
        if (check) {
            usedTime = Date.now() - startTime;
            soundLose.play();
            debugMsg = 'lose';
        }

        check = checkWin();
        if (check) {
            usedTime = Date.now() - startTime;
            soundWin.play();
            debugMsg = 'win';
        }

    }

    constructor(param) {
        this.init(param);
    }

    logic() {
        this.counter = (this.counter + 1) & 0xffff;

        if (this.pos.length == 4 && this.counter >= 16) {
            lineState.init([this.pos[0], this.pos[1], this.pos[2], this.pos[3]]);
            return ST.LINE;
        }

        return -1;
    }

    draw() {
        ctx.clearRect(0, 0, cvs.clientWidth, cvs.clientHeight);
        drawBoard();
        if (this.pos.length <= 0) {
            return; // early exit
        }
        const FLASH_FRAME = 16; // must be power of 2
        // flash 2 colors
        if ((this.counter & FLASH_FRAME) == FLASH_FRAME) {
            ctx.strokeStyle = 'black';
        } else {
            ctx.strokeStyle = 'white';
        }
        ctx.lineWidth = 4;
        for (let i = 0; i < this.pos.length; i += 2) {
            let x, y;
            x = px(this.pos[i]) + 5;
            y = py(this.pos[i + 1]) + 5;
            ctx.strokeRect(x, y, RWIDTH - 10, RHEIGHT - 10);
        }
    }

    /**
     * 
     * @param {MouseEvent} event 
     */
    onClick(event) {
        let x, y;
        let gx, gy;  // c, r
        let valid = false;
        x = event.offsetX;
        y = event.offsetY;

        // button first
        if (x >= BUTTON_RESTART_X && x <= BUTTON_RESTART_X2 && y >= BUTTON_RESTART_Y && y <= BUTTON_RESTART_Y2) {
            console.log('RESTARTing...seed=' + myRandom.seed);
            init();
        }

        // skip if pos is full, wait to change state
        if (this.pos.length == 4) {
            return;
        }

        gx = gridX(x);
        gy = gridY(y);
        valid = gridValid(gx, gy);
        debugMsg = 'x, y, gx, gy=' + x + ',' + y + ' | ' + gx + ',' + gy + '  v=' + valid;
        // console.log(debugMsg);

        // early exit when gx, gy is invalid
        if (valid == false) {
            return;
        }

        switch (this.pos.length) {
            case 0:  // empty, fill up gx, gy
                this.pos[0] = gx;
                this.pos[1] = gy;
                break;

            case 2: // one pos (a pair)
                // check if pos is the same, cancel it
                if (this.pos[0] == gx && this.pos[1] == gy) {
                    this.pos = []; 
                    break;
                }

                if (board[gx][gy] != board[this.pos[0]][this.pos[1]]) {
                    debugMsg = '不同颜色 Color NOT same';
                    this.pos = [];
                    soundError.play();
                    break;
                }
                // TODO core logic, need to check 2 pos can be eliminated
                this.pos[2] = gx;
                this.pos[3] = gy;

                let pair;
                pair = checkPair(this.pos[0], this.pos[1], this.pos[2], this.pos[3]);
                if (pair) {
                    this.counter = 0; // white when counter == 0
                    this.draw();
                    debugMsg = '可以 YES:' + this.pos[0] + ',' + this.pos[1] + ' | ' + this.pos[2] + ',' + this.pos[3];
                    // let logic jump to another stage after certain time
                } else {
                    debugMsg = '不能 no:' + this.pos[0] + ',' + this.pos[1] + ' | ' + this.pos[2] + ',' + this.pos[3];
                    soundError.play();
                    this.pos = [];
                }
                break;

            default:
                // cancel all
                this.pos = [];
                break;

        }


    }
}
let selectState = new Select([]);



function logic() {
    switch (state) {
        case ST.SELECT:
            return selectState.logic();

        case ST.LINE:
            return lineState.logic();

        case ST.DROP:
            return dropState.logic();
    }

    return -1; // no state change by default
}


/**
 * 
 * @param {MouseEvent} event 
 */
function onClick(event) {
    switch (state) {
        case ST.SELECT:
            selectState.onClick(event);
    }
}

function draw() {
    let newState;
    newState = logic();
    if (newState >= 0) {
        state = newState;
    }
    // ctx.clearRect(0, 0, cvs.clientWidth, cvs.clientHeight);
    // drawBoard();
    switch (state) {
        case ST.SELECT:
            selectState.draw();
            break;

        case ST.LINE:
            lineState.draw();
            break;

        case ST.DROP:
            dropState.draw();
            break;
    }

    drawState(); // state, timer etc
    drawButtonRestart();
    raf = requestAnimationFrame(draw);
}


function initMouseEvent() {
    // cvs event is ok, no need to have window event (same?)
    cvs.addEventListener("click", onClick);
}


function init() {
    // there is NO random seed in Javascript built-in Math.random()
    // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
    board = [];

    // fill up the 2d array first
    for(let c = 0; c < COL; c++){
         board[c] = [];
    }

    let total = ROW * COL;
    // console.log('total=', total);
    for (let i = 0; i<total; i += 2) {
        let color = myRandom(MAX_COLOR);
        let avaCol = []; // available column, non-full

        for (let c = 0; c < COL; c++) {
            if (board[c].length < ROW) {
                avaCol[avaCol.length] = board[c];
            }
        }
        // console.log('i, ava.len=', i, avaCol.length);
        let c1, c2;
        // only one column is non-full, put them all, no random
        if (avaCol.length==1) {
            c1 = 0;
            c2 = 0;
        } else {
            [c1, c2] = randomTwoDiff(avaCol.length);
        }
        // console.log('c1, c2=', c1, c2);
        avaCol[c1][avaCol[c1].length] = color;
        avaCol[c2][avaCol[c2].length] = color;
    }

    // for testing only
    // board[0] = [0, 1, 1];
    // board[1] = [1, 0, 1];

    startTime = Date.now();
    initMouseEvent();
    if (raf != undefined) {
        // if we do not cancel last animation
        // the screen draw will become faster, why?
        window.cancelAnimationFrame(raf);
    }
    raf = requestAnimationFrame(draw);
}


// keep test case
testBigSmall();
// testDrawLine(); // standalone test, no init()
// testCheckPair(); // standalone test, no init();

// core logic, DON'T remove, only comment
// include start animation : raf = ...
// myRandom.seed = 27978;
init();
